﻿#region Copyright information
// 
// Copyright © 2005-2013 Yongkee Cho. All rights reserved.
// 
// This code is a part of the Biological Object Library and governed under the terms of the
// GNU Lesser General  Public License (LGPL) version 2.1 which accompanies this distribution.
// For more information on the LGPL, please visit http://bol.codeplex.com/license.
// 
// - Filename: BinomialDistribution.cs
// - Author: Yongkee Cho
// - Email: yongkeecho@gmail.com
// - Date Created: 2012-09-06 11:39 AM
// - Last Modified: 2013-01-25 3:59 PM
// 
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using BOL.Maths.Functions;

namespace BOL.Maths.Distributions
{
    /// <summary>
    /// Represents a binomial distribution with n trials and success probability p.
    /// <see>Mathematica: ref/BinomialDistribution</see>
    /// <see>http://en.wikipedia.org/wiki/Binomial_distribution</see>
    /// <typeparam name="TKey"></typeparam>
    /// </summary>
    public sealed class BinomialDistribution<TKey> : NomialDistributionBase<TKey>, IUnivariateDistribution<int>, IEquatable<BinomialDistribution<TKey>>
    {
        #region Private variables

        private int _n; // the number of trials
        private double _p; // probability of outcome 1
        private double _q; // probability of outcome 2;

        #endregion

        #region Public properties

        /// <summary>Gets or sets the number of trials of a binomial distribution.</summary>
        public int N { get { return _n; } set { _n = value; } }

        /// <summary>Gets the range of a binomial distribution.</summary>
        public IRange<int> Domain { get { return new Range<int>(0, _n); } }

        /// <summary>Gets the mean of a binomial distribution.</summary>
        public double Mean { get { return _n * _p; } }

        /// <summary>Gets the median of a binomial distribution.</summary>
        public double Median { get { return Math.Floor(_n * _p); } }

        /// <summary>Gets the mode of a binomial distribution.</summary>
        public double Mode { get { return Math.Floor((_n + 1) * _p); } }

        /// <summary>Gets the variance of a binomial distribution.</summary>
        public double Variance { get { return _n * _p * _q; } }

        /// <summary>Gets the entropy of a binomial distribution.</summary>
        public double Skewness { get { return (1 - 2 * _p) / Math.Sqrt(_n * _p * _q); } }

        /// <summary>Gets the kurtosis of a binomial distribution.</summary>
        public double Kurtosis { get { return (1 - 6 * _p * _q) / (_n * _p * _q); } }

        /// <summary>Gets the entropy of a binomial distribution.</summary>
        public double Entropy { get { throw new Exception("Entropy is undefined for binomial distribution."); } }

        #endregion

        #region Constructors

        /// <summary>
        /// Instantiates a binomial distribution.
        /// </summary>
        /// <param name="probabilityTable">Probability table.</param>
        /// <param name="n">The number of trials.</param>
        public BinomialDistribution(IDictionary<TKey, double> probabilityTable, int n)
            : base(probabilityTable)
        {
            if (ProbabilityTable.Count != 2)
                throw new ArgumentException("Binomial distribution can have two outcomes", "probabilityTable");

            _p = ProbabilityTable.ElementAt(0).Value;
            _q = ProbabilityTable.ElementAt(1).Value;

            if (n < 0)
                throw new ArgumentOutOfRangeException("n", "n must be a positive integer.");
            _n = n;
        }

        public BinomialDistribution(TKey success, TKey fail, int n)
            : this(new Dictionary<TKey, double> { { success, 0.5 }, { fail, 0.5 } }, n) { }

        #endregion

        #region INomialDistribution<TKey> implementation

        /// <summary>Gets or sets the frequency table of a Binomial distribution.</summary>
        public override IDictionary<TKey, double> ProbabilityTable
        {
            get { return base.ProbabilityTable; }
            set
            {
                base.ProbabilityTable = value;

                if (value.Count != 2)
                    throw new ArgumentException("Binomial distribution can have two outcomes", "value");

                _p = ProbabilityTable.ElementAt(0).Value;
                _q = ProbabilityTable.ElementAt(1).Value;
            }
        }

        #endregion

        #region ICloneable implementation

        public BinomialDistribution<TKey> Clone()
        {
            return new BinomialDistribution<TKey>(ProbabilityTable, _n);
        }

        object ICloneable.Clone()
        {
            return Clone();
        }
        
        #endregion

        #region Public methods

        /// <summary>
        /// Returns the binomial density given value.
        /// </summary>
        /// <param name="value"></param>
        /// <returns>binomial density</returns>
        public double Pdf(int value)
        {
            if (0 < value || value > _n)
                throw new ArgumentException("value must be in interval [0, n].");

            return CountingFunctions.Binomial(_n, value) * Math.Pow(_p, value) * Math.Pow(_q, _n - value);
        }

        /// <summary>
        /// Returns the cummulative binomial density given value.
        /// </summary>
        /// <param name="value"></param>
        /// <returns>cummulative binomial density</returns>
        public double Cdf(int value)
        {
            return Enumerable.Range(0, value + 1).Sum(x => Pdf(x));
        }

        /// <summary>
        /// Returns a random value from the distribution.
        /// <see>http://people.csail.mit.edu/milch/blog/index.html</see>
        /// </summary>
        /// <returns>random value</returns>
        public int Sample(Random r)
        {
            var logQ = -Math.Log(_q);
            var sum = 0.0;
            var i = 0;

            while (sum <= logQ)
            {
                var u = r.NextDouble();
                var e = -Math.Log(u); // exponential random variate
                sum += e / (_n - i);
                i++;
            }

            return i - 1;
        }

        /// <summary>
        /// Returns the value of the binomial distribution for given probability.
        /// </summary>
        /// <param name="p">probability</param>
        /// <returns></returns>
        public int Quantile(double p)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Estimates parameters of the Bernoulli distribution using maximum likelihood.
        /// </summary>
        public new void MaximumLikelihoodEstimate(IEnumerable<TKey> source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var list = source.ToList();
            base.MaximumLikelihoodEstimate(list);
            N = list.Count();
        }

        public void MaximumLikelihoodEstimate(IEnumerable<int> source)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Estimates parameters of the Bernoulli distribution using Bayesian methods.
        /// <param name="priorParameters">Dirichlet priors</param>
        /// </summary>
        public new void BayesianEstimate(IEnumerable<TKey> source, params dynamic[] priorParameters)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var list = source.ToList();
            base.BayesianEstimate(list);
            N = list.Count();
        }

        public void BayesianEstimate(IEnumerable<int> source, params dynamic[] priorParameters)
        {
            throw new NotSupportedException();
        }

        #endregion

        #region IEquatable<BinomialDistribution<TKey>> implementation

        public bool Equals(BinomialDistribution<TKey> other)
        {
            return _n.Equals(other._n) & _p.Equals(other._p);
        }

        #endregion

        #region Object overriden

        public override int GetHashCode()
        {
            return ProbabilityTable.Aggregate(1, (hashCode, p) => hashCode ^ p.Value.GetHashCode()) ^ N.GetHashCode();
        }

        public override bool Equals(object other)
        {
            if (other == null)
                throw new ArgumentNullException("other");

            if (!(other is BinomialDistribution<TKey>))
                throw new InvalidCastException("The 'other' argument is not a BinomialDistribution<TKey> object.");
            
            return Equals(other as BinomialDistribution<TKey>);
        }

        public override string ToString()
        {
            return String.Format("Binomial(n = {0}, {1})", _n, String.Join(", ", ProbabilityTable.Select(p => String.Format("p_{0} = {1:0.0000}", p.Key, p.Value))));
        }

        #endregion
    }
}
